package cn.tedu.charging.order.service;

import cn.tedu.chaging.common.pojo.vo.ChargingProcessVO;
import cn.tedu.chaging.common.pojo.vo.StationInfoVO;
import cn.tedu.chaging.common.pojo.vo.UserInfoVO;
import cn.tedu.chaging.common.pojo.param.GunStatusUpdateParam;
import cn.tedu.chaging.common.web.JsonResult;
import cn.tedu.charging.order.constant.Constant;
import cn.tedu.charging.order.dao.repository.OrderRepository;
import cn.tedu.charging.order.feign.CostClient;
import cn.tedu.charging.order.feign.DeviceClient;
import cn.tedu.charging.order.feign.UserClient;
import cn.tedu.charging.order.job.DeviceCheckJob;
import cn.tedu.charging.order.mqtt.MqttProducer;
import cn.tedu.charging.order.pojo.param.ChargingResultParam;
import cn.tedu.charging.order.pojo.dto.CarInfoDto;
import cn.tedu.charging.order.pojo.dto.ChargingDto;
import cn.tedu.charging.order.pojo.dto.StationInfoDto;
import cn.tedu.charging.order.pojo.param.OrderAddParam;
import cn.tedu.charging.order.pojo.po.ChargingBillSuccessPO;


import cn.tedu.charging.order.pojo.po.OrderMQPO;
import cn.tedu.charging.order.pojo.vo.OrderDetailVO;
import cn.tedu.charging.order.rabbitmq.RabbitMQOrderProducer;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.math.BigDecimal;
import java.util.List;
import java.util.Random;

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    /**
     * mqtt消息发送工具
     */
    @Autowired
    private MqttProducer mqttProducer;

    /**
     * rabbit-mq 消息发送工具
     */
    @Autowired
    private RabbitMQOrderProducer rabbitMQOrderProducer;

    /**
     * 数据操作层
     */
    @Autowired
    private OrderRepository orderRepository;


    /**
     * 访问用户服务的feign客户端
     */
    @Autowired
    private UserClient userClient;

    /**
     * 访问设备服务的feign客户端
     */
    @Autowired
    private DeviceClient deviceClient;

    /**
     * 访问计价服务的feign客户端
     */
    @Autowired
    private CostClient costClient;


    /**
     * @param orderAddParam
     * @return 1 扫码
     * 2 判断余额 余额够 继续
     * 3 创建订单
     * 4 给设备下发指令 开始充电  应该选择哪种通信方式 该怎么告诉设备开始充电
     * 同步
     * 优点
     * 站在发送者的角度  比较简单 好理解
     * 10:01
     * Boolean success = startCharging() //发生充电指令
     * if (success) { //判断是否开始充电
     * 10:05
     * insertOrderData();//创建订单 数据库有记录
     * return orderNo;
     * }
     * 缺点
     * <p>
     * 站在接收者的角度
     * * 设备有多种情况
     * *  1 没坏 收到了,但是因为网络原因,没有很快响应 告诉接受者
     * *  2 坏了 导致设备收不到,  没有很快响应 告诉接受者
     * *
     * * 发送者不知道设备的状态 是能充还是不能充 等待 页面在转
     * <p>
     * 异步
     * startCharging();//发生充电指令 发送者
     * insertOrderData();//创建订单 数据库有记录
     * return orderNo;
     * 优点
     * <p>
     * 缺点
     * 上面的代码没有实时得到充电桩的状态 能充还是不能充
     * 得有方案来接收 充电桩的状态
     * <p>
     * 接受者
     * 发送完充电指令后,继续执行后面的业务逻辑,当充电桩有响应
     * 正常 开始充电
     * 异常 设备故障  无响应
     * 做后续的处理
     * 开始充电 计费
     * 设备故障  无响应 订单取消,通知用户
     * <p>
     * 和设备通信  MQTT 物联网技术  类似与消息队列  异步和解耦
     * <p>
     * 5 创建订单
     * 6 创建订单编号 唯一  能不能用数据库的自增主键?  1 2 3
     * 不用数据库主键 分库
     * 雪花算法  自己了解 分布式的id生成器
     * UUID 没有顺序,mysql是建议有顺序 , uuid + 时间戳
     * 自定义   城市编号 + 随机数 + 时间
     * <p>
     * 7  通过枪id获取运营商信息和充电站信息
     * 8  通过用户id获取用户车辆信息
     * 9  通过枪的id修改枪的状态为已使用 充电的时候修改为已经使用,充完后要把枪的状态修改为未使用(空闲)
     * 10 插入订单数据
     * 11 返回订单编号
     */
    @Override
    public String createOrder(OrderAddParam orderAddParam) throws Exception {
        //todo 订单创建的流程 自定向下编程
        // 1 创建订单编号
        String orderNo = getOrderNo();
        log.debug("生成订单号:{}", orderNo);
        log.debug("生成设备自检任务:{}", orderNo);
        //创建自检定时任务
        DeviceCheckJob job = new DeviceCheckJob(orderNo, orderAddParam.getGunId());
        log.debug("生成设备自检任务成功:{},出参:{}", orderNo, job);
        // 2 通过枪id获取运营商信息和充电站信息 100ms
        log.debug("获取运营商和场站信息-入参:{}", orderAddParam.getGunId());
        StationInfoDto stationInfoDto = getStationInfo(orderAddParam.getGunId());
        log.debug("获取运营商和场站信息-入参:{},出参:{}", orderAddParam.getGunId(), stationInfoDto);
        // 3 通过用户id获取用户车辆信息 80ms
        log.debug("获取用户车辆信息-入参:{}", orderAddParam.getUserId());
        CarInfoDto carInfoDto = getCarInfo(orderAddParam.getUserId());
        log.debug("获取用户车辆信息-入参:{},出参:{}", orderAddParam.getUserId(), carInfoDto);
        // 4 通过枪的id修改枪的状态为已使用 80ms
        //2表示已使用 就传2
        log.debug("设置充电枪状态为已经使用-入参:枪id-{},状态-{}", orderAddParam.getGunId(), Constant.GUN_STATS_BUSY);
        Boolean success = updateGunStatus(orderAddParam.getGunId(), Constant.GUN_STATS_BUSY);// 2 魔数
        log.debug("设置充电枪状态为已经使用-入参:枪id-{},状态-{},出参:{}", orderAddParam.getGunId(), Constant.GUN_STATS_BUSY, success);
        // 1表示空闲
        //updateGunStatus(orderAddParam.getGunId(),1);

        //updateGunStatusOff(orderAddParam.getGunId());
        //updateGunStatusOn(orderAddParam.getGunId());
        if (!success) {
            log.warn("修改枪状态失败,枪id:{},状态:{}", orderAddParam.getGunId(), Constant.GUN_STATS_BUSY);
        }
        //5 给充电桩发送充电指令
        startCharging(orderNo, orderAddParam.getPileId(), orderAddParam.getGunId());
        //6 充电开始后,
        // 正常情况 设备会定时和订单服务同步数据,温度,电压,用电度数
        // 异常情况 设备接收到开始充电指令后,开始充电,充了一会,设备故障,不会再和订单服务同步数据
        // 订单需要处理这种数据
        // 1 延迟消息  rabbit-mq 今天晚上要把rabbit-mq安装成功,
        // 1 安装手册,2 用上个阶段的虚拟机也是OK
        // 2 定时任务
        // 总结 场景/问题/解决

        //发送消息 1个小时
        sendOrder(orderNo);

        //设备自检
        //充电的步骤 1 生成订单号,2 调其他服务用户服务,设备服务,3 发送充电指令
        // 正常是没问题的
        // 异常 设备本身故障,充电的步骤 1 2 3 设备的自检
        //延迟处理的一种逻辑  通过rabbit-mq的延迟消息可以处理
        // 1 定时任务 quartz 定时任务的框架 类似闹钟

        //直接给设备发送开始充电指令,有可能设备是故障的,他不理你
        //给你2分钟时间,你去自检,自检后,我再给你发送充电指令

        //把中间状态的订单数据 变为 终态 除了用延迟消息
        // 还能用什么方案 ?
        return orderNo;
    }

    /**
     * 发送延迟消息
     *
     * @param orderNo
     */
    private void sendOrder(String orderNo) {
        OrderMQPO orderMQPO = new OrderMQPO();
        orderMQPO.setOrderNo(orderNo);
        rabbitMQOrderProducer.sendOrder(orderMQPO);
    }


    /**
     * 保存成功订单
     */
    @Override
    public void saveSuccessOrder(ChargingResultParam deviceResultParam) {
        ChargingBillSuccessPO successPO = new ChargingBillSuccessPO();
        successPO.setBillId(deviceResultParam.getOrderNo());
        orderRepository.saveSuccessOrder(successPO);
    }

    @Override
    public void saveFailOrder() {

    }

    @Override
    public List<ChargingBillSuccessPO> orderList(Integer userId) {
        List<ChargingBillSuccessPO> chargingBillSuccessPOS =
                orderRepository.orderList(userId);
        return chargingBillSuccessPOS;
    }

    /**
     * 正常停止
     * @param orderNo
     * @return
     */
    @Override
    public OrderDetailVO normalStop(String orderNo) {
        //通过订单号获取成功订单详情
        ChargingBillSuccessPO  chargingBillSuccessPO = getOrderInfoByOrderNo(orderNo);
        if (chargingBillSuccessPO != null) {
            //订单存在
            //获取订单花费
            ChargingProcessVO chargingProcessResult = getOrderCostByOrderNo(orderNo);
            if (chargingProcessResult != null) {
                //todo 给充电桩发送停止充电指令 先做,
                // 如果最给充电桩发送停止指令 回到算完钱后,还会产生新的费用
                // 先停设备 后算钱
                //更新订单状态
                updateOrderStatus2NormalStop(orderNo);
                //更新设备状态
                updateGunStatus(chargingBillSuccessPO.getGunId(),Constant.GUN_STATS_FREE);
                //获取用户最新的余额
                BigDecimal userBalance = getUserBalance(chargingBillSuccessPO.getUserId());
                //扣钱  用户最新的余额 -  订单花费
                BigDecimal currentBalance = userBalance.subtract(chargingProcessResult.getTotalCost());
                //更新用户的余额 事务的问题 分布式事务
                //本地事务是指 对同一个数据库多次的更新操作 要么全部成功,要么全部失败
                //分布式事务  多个服务之间 多个数据库 之间多个更新操作  要么全部成功,要么全部失败
                //最新的余额更新到用户的账户
                updateUserBalance(chargingBillSuccessPO.getUserId(),currentBalance);
                //返回扣除当前订单话费的余额
                OrderDetailVO orderDetailVO = buildOrderDetailVO(orderNo, currentBalance, chargingProcessResult);
                return orderDetailVO;
            }else {
                //抛个自定义异常 订单获取价格信息失败 todo
            }
        }else {
            //抛个自定义异常 订单不存在 todo
        }
        return new OrderDetailVO();
    }

    /**
     * 更新用户余额
     * @param userId
     * @param currentBalance
     */
    private void updateUserBalance(Integer userId, BigDecimal currentBalance) {
        log.debug("更新用户余额,用户id:{},最新余额:{}",userId,currentBalance);
        //1调用UserClient http open-feign 直接调用
        //2 通过消息队列 订单服务给用户服务发一条扣款消息,用户服务消费扣款消息,
        //3 需要考虑重复幂等,需要考虑消息丢失  目标 不能不扣(消息丢失),不能多扣(消息重复),不能少扣(消息丢失)
    }

    /**
     * 构建正常停止充电返回结果
     * @param orderNo
     * @param currentBalance
     * @param chargingProcessResult
     * @return
     */
    private  OrderDetailVO buildOrderDetailVO(String orderNo, BigDecimal currentBalance, ChargingProcessVO chargingProcessResult) {
        OrderDetailVO orderDetailVO = new OrderDetailVO();
        orderDetailVO.setOrderNo(orderNo);
        orderDetailVO.setBalance(currentBalance);
        orderDetailVO.setTotalCost(chargingProcessResult.getTotalCost());
        orderDetailVO.setHours(chargingProcessResult.getHours());
        orderDetailVO.setMinutes(chargingProcessResult.getMinutes());
        orderDetailVO.setSeconds(chargingProcessResult.getSeconds());
        return orderDetailVO;
    }

    /**
     * 通过订单号查询订单信息
     * @param orderNo
     * @return
     */
    private ChargingBillSuccessPO getOrderInfoByOrderNo(String orderNo) {
        return orderRepository.getSuccess(orderNo);
    }

    /**
     * 获取当前用户最新余额
     * @param userId
     * @return
     */
    private BigDecimal getUserBalance(Integer userId) {
        log.debug("调用用户服务获取用户余额:{}",userId);
        JsonResult<BigDecimal> userBalance = userClient.getUserBalance(userId);
        log.debug("调用用户服务获取用户余额:入参:{},出参:{}",userId,userBalance);
        if (userBalance != null) {
            return userBalance.getData();
        }
        return new BigDecimal(0);
    }

    /**
     * 通过订单号获取当前订单花费
     * @param orderNo
     * @return
     * @throws Exception
     */
    private void updateOrderStatus2NormalStop(String orderNo) {
        orderRepository.updateOrderStatusFromProcess2NormalEnd(orderNo);
    }

    /**
     * 通过订单号获取当前订单花费
     * @param orderNo
     * @return
     * @throws Exception
     */
    private ChargingProcessVO getOrderCostByOrderNo(String orderNo) {
        JsonResult<ChargingProcessVO> costResult = costClient.getCostByOrderNo(orderNo);
        if (costResult != null) {
            return  costResult.getData();
        }
        return null;
    }

    /**
     * 异步下发充电指令给充电桩
     *
     * @param orderNo 订单号
     * @param pileId  桩id
     * @param gunId   枪id
     */
    // 方法定义入参的编码思想
    //private void startCharging(OrderAddParam orderAddParam) {}
    private void startCharging(String orderNo,
                               Integer pileId,
                               Integer gunId) {
        log.debug("发送指令:订单号-{},桩id-{},枪id-{}", orderNo, pileId, gunId);
        ChargingDto chargingDto = new ChargingDto();
        chargingDto.setGunId(gunId);
        chargingDto.setPileId(pileId);
        //开始充电的指令  指令应该越小越好
        // msg表示的是给充电桩发送的指令,有多个,比如start 开始 stop 结束
        chargingDto.setMsg(Constant.START_CHARGING);
        chargingDto.setOrderNo(orderNo);
        log.debug("发送充电指令-入参:{}", chargingDto);
        mqttProducer.send(chargingDto);

    }

    /**
     * 更新枪的状态
     *
     * @param gunId
     * @param status
     * @return
     */
    private Boolean updateGunStatus(Integer gunId, Integer status) {
        GunStatusUpdateParam param = new GunStatusUpdateParam();
        param.setId(gunId);
        param.setStatus(status);
        JsonResult<Boolean> jsonResult = deviceClient.updateStationStatus(param);
        if (jsonResult != null) {
            return jsonResult.getData();
        }
        return false;
    }


    /**
     * 通过feign的方式调用用户服务
     *
     * @param userId
     * @return
     */
    private CarInfoDto getCarInfoByFeign(Long userId) {
        JsonResult<UserInfoVO> userInfo = userClient.getUserInfo(userId);
        if (userInfo != null) {
            UserInfoVO data = userInfo.getData();
            if (data != null) {
                CarInfoDto carInfoDto = new CarInfoDto();
                BeanUtils.copyProperties(data, carInfoDto);
                return carInfoDto;
            }
        }
        return null;
    }


    @Autowired
    private RestTemplate restTemplate;


    /**
     * 通过随机算法实现负载均衡
     *
     * @return
     */
    public String chooseUrl() {
        //已知有3个用户服务
        // 动态感知服务的变化 包括上线和上线 前提是订单服务不需要重新启动
        // 注册中心  /  服务发现
        //List<String> servers = nocos.getUrl("userService")
        // random 选中一个
        // feign
        String url = "http://localhost:8083/user/info/{1}"; //占位符号
        String url1 = "http://localhost:8084/user/info/{1}"; //占位符号
        String url2 = "http://localhost:8085/user/info/{1}"; //占位符号
        String url3 = "http://localhost:8086/user/info/{1}";
        String url4 = "http://localhost:8086/user/info/{1}";

        //负载均衡算法 简单实现随机
        //ArrayList 和数组有什么区别? 自己去实现基于ArrayList

        String[] servers = new String[3];
        servers[0] = url;
        servers[1] = url1;
        servers[2] = url2;
        Random random = new Random();
        int r = random.nextInt(servers.length);
        String server = servers[r];
        return server;
    }

    /**
     * 通过用户id获取用户绑定的车辆信息
     * 先假定一个用户只能绑定一辆车 充电的时候就是用户绑定的那辆唯一的车
     * 如果后续功能迭代 充电的时候由用户选择给哪辆车充电, 车的id可以通过入参的方式传入
     *
     * @param userId 高内聚 单一职责 导致 低耦合
     * @return
     */
    private CarInfoDto getCarInfo(Long userId) {
        return getCarInfoByFeign(userId);
    }

    /**
     * 通过RestTemplate 来调用用户服务
     *
     * @param userId
     * @return
     */
    private CarInfoDto getCarInfoByRestTemplate(Integer userId) {
        String url = chooseUrl();
        log.debug("通过restTemplate调用用户接口获取用户信息-获取url:{}", url);
        //String url = "http://localhost:8083/user/info/"+userId;
        log.debug("通过restTemplate调用用户接口获取用户信息-入参:{}", userId);
        ParameterizedTypeReference<JsonResult<UserInfoVO>> reposeBody =
                new ParameterizedTypeReference<JsonResult<UserInfoVO>>() {
                };
        ResponseEntity<JsonResult<UserInfoVO>> entity =
                restTemplate.
                        exchange(url,
                                HttpMethod.GET,
                                null,
                                reposeBody,
                                userId);
        log.debug("通过restTemplate调用用户接口获取用户信息-入参:{},出参:{}", userId, entity);

        if (entity != null) {
            HttpStatus statusCode = entity.getStatusCode();
            if (HttpStatus.OK == statusCode) {
                JsonResult<UserInfoVO> body = entity.getBody();
                UserInfoVO data = body.getData();
                log.debug("通过restTemplate调用用户接口获取用户信息-入参:{},body:{}", userId, body);
                CarInfoDto carInfoDto = new CarInfoDto();
                Integer carId = data.getCarId();
                carInfoDto.setCarId(carId); //如果参数少可以同手动设置
                BeanUtils.copyProperties(data, carInfoDto);
                return carInfoDto;
            }
        }
/*
        CarInfoDto carInfoDto = new CarInfoDto();
        carInfoDto.setCarId(15);
        return carInfoDto;*/
        return null;
    }


    /**
     * 通过枪id获取运营商信息和充电站信息
     *
     * @param gunId
     * @return StationInfoDto
     */
    private StationInfoDto getStationInfo(Integer gunId) {
        JsonResult<StationInfoVO> stationInfo = deviceClient.getStationInfo(gunId);
        if (stationInfo != null) {
            StationInfoVO data = stationInfo.getData();
            if (data != null) {
                StationInfoDto dto = new StationInfoDto();
                BeanUtils.copyProperties(data, dto);
                return dto;
            }
        }
        return null;
    }

    /**
     * 生成订单号
     * 固定开始的 10000 + _ + 随机数 + _ + 时间戳
     *
     * @return
     */
    private String getOrderNo() {
        //换一种算法 雪花
        String start = "10000";
        long now = System.currentTimeMillis();
        Random random = new Random();
        int r = random.nextInt(100);
        String orderNo = start + "_" + r + "_" + now;
        return orderNo;
    }
}